package simplex

/**
 * Clase que define un tipo matriz y algunas de sus
 * operaciones mas usadas o importantes.
 * 
 * @author age, joaquinmonleon
 *
 */
class Matrix(els: Array[Array[Racional]]) {  
  
  // Elementos de la matriz
  //val elements: Array[Array[Racional]] = els
  var elements: Array[Array[Racional]] = els
  
  // Numero de filas y columnas
  def nRows: Int = elements.length  
  def nCols: Int = if (elements.isEmpty) 0   
                   else elements.head.length  
   
  require(elements.forall(_.length == nCols))
  
  
  // Salida entrada de nuevas filas / columnas
  def getRow (i: Integer) = elements (i)
  def putRow (i: Integer, newRow: Array[Racional]) = { elements (i) = newRow }
  
  def getCol (i: Integer): Array[Racional] = {
    var a:Array[Racional] = Array.fill(nRows)(0.0)
    for (j <- 0 until nRows) {
      a(j) = elements(j)(i);
    }
    a;
  }
  def getColMatrix (i: Integer): Matrix = 
    Matrix.createOneColMatrix(getCol (i))
  
  def removeCol (i: Integer): Array[Racional] = {
    var col = getCol (i)
      
    for (j <- 0 until nRows) {
      var a = elements(j)
      
      var (a1, temp) = a.splitAt(i)
      if (temp.length > 1) {
        var (temp2, a2) = temp.splitAt(1)
        a = a1 ++ a2
      } else {
        a = a1
      }
      
      elements(j) = a
    }
    
    col
  }

  def removeRow (n: Integer): Array[Racional] = {
    var row = getRow(n)
    
    var newm = Matrix.getEmptyMatrix(nRows-1, nCols)
    
    var reduce = 0 
    for (i <- 0 until nRows) {
      if (i == n) {
        reduce = 1
      } else {
        for (j <- 0 until nCols) {
          newm.elements(i-reduce)(j) = elements(i)(j)
        }
      }
    }
    
    elements = newm.elements
    
    row
  }
  
  def getOneRowArray (indexes: Array[Integer]): Array[Racional] = {
    var arr: Array[Racional] = new Array (indexes.length)
    
    for (i <- 0 until indexes.length) {
      arr (i) = elements(0)(indexes(i))
    }
    
    arr
  }
  
  def getValue (): Racional =
    elements(0)(0)
  
  def getColValue (i: Integer): Racional =
    elements(0)(i)
    
  // Operaciones sobre filas
  def addRows (r1: Integer, r2: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) + elements (r2)(i)
	}
  }

  def subRows (r1: Integer, r2: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) - elements (r2)(i)
	}
  }
  
  def mulRows (r1: Integer, r2: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) * elements (r2)(i)
	}
  }

  def divRows (r1: Integer, r2: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) / elements (r2)(i)
	}
  }

  def addRowScalar (r1: Integer, scalar: Racional) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) + scalar
	}
  }

  def subRowScalar (r1: Integer, scalar: Racional) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) - scalar
	}
  }

  def mulRowScalar (r1: Integer, scalar: Racional) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) * scalar
	}
  }

  def divRowScalar (r1: Integer, scalar: Racional) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = elements (r1)(i) / scalar
	}
  }

  def subRowScalar (scalar: Racional, r1: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = scalar - elements (r1)(i)
	}
  }

  def divRowScalar (scalar: Racional, r1: Integer) = {
	for (i <- 0 until elements(r1).length) {
	  elements (r1)(i) = scalar / elements (r1)(i)
	}
  }

  
  def swapRows(r1: Integer, r2: Integer) = {
    var temp = elements (r1)
    elements (r1) = elements (r2)
    elements(r2) = temp
  }

  // Operaciones avanzadas
  def transpuesta (): Matrix =
    Matrix.transpuesta(this)
  
  def inversa (): Matrix = 
    Matrix.inversa(this)
  
  def adjunta (): Matrix =
    Matrix.adjunta(this)
    
  def determinante (): Racional =
    Matrix.determinante(this)

  def cofactores (): Matrix =
    Matrix.cofactores(this)

  def redim(rows: Integer, cols: Integer): Matrix = {
    var newm = Matrix.getEmptyMatrix(rows, cols)
    
    var minr = if (nRows < newm.nRows) nRows else newm.nRows
    var minc = if (nCols < newm.nCols) nCols else newm.nCols
    
    for (i <- 0 until minr) {
      for (j <- 0 until minc) {
        newm.elements(i)(j) = elements(i)(j)
      }
    }
    
    newm
  }
  
  // Metodos redefinidos
  override def toString(): String = {  
    val rowStrings =   
      for (row <- elements)   
        yield row.mkString("[", ", ", "]")  
    rowStrings.mkString("", "\n", "")  
  }
  
  override def clone (): Matrix = 
    new Matrix(elements.map(_.clone))  
}

object Matrix {

  // Metodos factoria
  def getEmptyMatrix(rows: Integer, cols: Integer): Matrix =
    new Matrix(Array.fill(rows, cols)(0.0))

  def redim(m: Matrix, rows: Integer, cols: Integer): Matrix = 
    m.redim(rows, cols)
  
  def getBMatrix (tam: Integer): Matrix = {
    var m: Matrix = getEmptyMatrix(tam, tam)
    for (i <- 0 until tam) {
      m.elements (i)(i) = 1.0
    }
    m
  }
  
  def createOneRowMatrix (a: Array[Racional]): Matrix = 
    new Matrix(Array(a))
  
  def createOneColMatrix (a: Array[Racional]): Matrix = {
    var m: Matrix = getEmptyMatrix(a.length, 1)
    for (i <- 0 until m.nRows) {
      m.elements(i)(0) = a(i)
    }
    m
  }
    
  // Operaciones sobre matrices y escalares
  def mXm (m1: Matrix, m2: Matrix): Matrix = {
    require(m1.nCols == m2.nRows)
    
    var nVals = m1.nCols
    var rows = m1.nRows
    var cols = m2.nCols
    
    var result = Matrix.getEmptyMatrix(rows, cols)
    
    for (i <- 0 until result.nRows) {
	  for (j <- 0 until result.nCols) {
	    var v:Racional = 0.0	    
	    for (k <- 0 until nVals) {
	      v += m1.elements(i)(k) * m2.elements(k)(j)
	    }
	    
	    result.elements(i)(j) = v
	  }
    }

    result
  }

  def mXs (m: Matrix, s: Racional): Matrix = {
    var result = m.clone()
    for (i <- 0 until result.nRows) {
	  for (j <- 0 until result.nCols) {
	    result.elements(i)(j) *= s 
	  }
    }
    result
  }
  
  def sXm (s: Racional, m: Matrix): Matrix =
    mXs (m, s)
  
  def mADDm (m1: Matrix, m2: Matrix): Matrix = {
    require(m1.nRows == m2.nRows && m1.nCols == m2.nCols)
    
    var rows = m1.nRows
    var cols = m1.nCols
    
    var result = Matrix.getEmptyMatrix(rows, cols)
    for (i <- 0 until result.nRows) {
	  for (j <- 0 until result.nCols) {
	    result.elements(i)(j) = 
	      m1.elements(i)(j) + m2.elements(i)(j)
	  }
    }
    
    result
  }

  def mADDs (m: Matrix, s: Racional): Matrix = {
    var rows = m.nRows
    var cols = m.nCols
    
    var result = Matrix.getEmptyMatrix(rows, cols)
    for (i <- 0 until result.nRows) {
	  for (j <- 0 until result.nCols) {
	    result.elements(i)(j) = 
	      m.elements(i)(j) + s
	  }
    }
    
    result
  }
  
  def sADDm (s: Racional, m: Matrix): Matrix = 
    mADDs(m, s)

  // Operaciones avanzadas
  def transpuesta (matrix: Matrix): Matrix = {
    var tras = Matrix.getEmptyMatrix(matrix.nCols, matrix.nRows)
    for (i <- 0 until matrix.nCols) {
      for (j <- 0 until matrix.nRows) {
        tras.elements(i)(j) = matrix.elements(j)(i)
      }
    }
    tras
  }

  def inversa (matrix: Matrix): Matrix = {
    var det = 1 / determinante(matrix)
    var adj = adjunta(matrix)
    sXm(det, adj)
  }
  
  def adjunta (matrix: Matrix): Matrix = 
    transpuesta(cofactores(matrix)) 
  
  def determinante (matrix: Matrix): Racional = {
    var det: Racional = 0
    
    if (matrix.nRows == 2) {
      det = (matrix.elements(0)(0) * matrix.elements(1)(1)) - (matrix.elements(0)(1) * matrix.elements(1)(0))
    } 
    else {
      var suma: Racional = 0.0
      
      for (i <- 0 until matrix.nRows) {
        var nm: Matrix = Matrix.getEmptyMatrix(matrix.nRows - 1, matrix.nCols - 1)
        for (j <- 0 until matrix.nRows) {
          if (j != i) {
            for (k <- 1 until matrix.nRows) {
              var indice: Integer = -1
              
              if (j < i) {
                indice = j
              } else if (j > i) {
                indice = j - 1
              }
              
              nm.elements(indice)(k - 1) = matrix.elements(j)(k)
            }
          }
        }
        
        if (i % 2 == 0) {
          suma += matrix.elements(i)(0) * determinante(nm)
        } else {
          suma -= matrix.elements(i)(0) * determinante(nm)
        }        
      }
      
      det = suma
    }
    
    det
  }

  def cofactores(matrix: Matrix): Matrix = {
    var nm: Matrix = Matrix.getEmptyMatrix(matrix.nRows, matrix.nCols)
    
	for (i <- 0 until matrix.nRows) {	  
	  for (j <- 0 until matrix.nRows) {
	    var det = Matrix.getEmptyMatrix(matrix.nRows - 1, matrix.nCols - 1)
	    var detValor: Racional = 0
		
	    for (k <- 0 until matrix.nRows) {
	      if (k != i) {
	        for (l <- 0 until matrix.nRows) {
	          if (l != j) {
	            var indice1 = if (k < i) k else (k - 1)
	            var indice2 = if (l < j) l else (l - 1)
	              
	            det.elements (indice1)(indice2) = matrix.elements(k)(l)
	          }
	        }
	      }
	    }

	    detValor = determinante(det)
	    nm.elements(i)(j) = detValor * Math.pow(-1, i + j + 2)
	  }
	}
    nm
  }
  
  // Operaciones sobre la estructura de matrices
  
  // Union de 2 matrices horizontalmente
  def mUmHoriz (m1: Matrix, m2: Matrix): Matrix = {
    
    require(m1.nRows == m2.nRows)
    
    var a1: Array[Array[Racional]] = m1.elements.map(_.clone)
    var a2: Array[Array[Racional]] = m2.elements.map(_.clone)
    
    for (i <- 0 until a1.length) {
      a1(i) = a1(i) ++ a2(i)
    }
    
    new Matrix(a1)
  }
}
